/*
 * A task scheduler that implements Monte-Carlo simulation
 *
 * Copyright 2010 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *   
 * $Id: EditDoubleCell.java 96 2011-08-14 20:15:48Z jchimene@gmail.com $
 */
package com.systasis.sked.client.view;

import com.google.gwt.cell.client.AbstractEditableCell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.i18n.client.NumberFormat;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.text.shared.SafeHtmlRenderer;
import com.google.gwt.text.shared.SimpleSafeHtmlRenderer;

public class EditDoubleCell extends AbstractEditableCell<Double, EditDoubleCell.ViewData> {

    interface Template extends SafeHtmlTemplates {
	@Template("<input type=\"text\" value=\"{0}\" tabindex=\"-1\"></input>")
	SafeHtml input(String value);
    }

    /**
     * The view data object used by this cell. We need to store both the text
     * and the state because this cell is rendered differently in edit mode. If
     * we did not store the edit state, refreshing the cell with view data would
     * always put us in to edit state, rendering a text box instead of the new
     * text string.
     */
    static class ViewData {

	private boolean isEditing;

	/**
	 * If true, this is not the first edit.
	 */
	private boolean isEditingAgain;

	/**
	 * Keep track of the original value at the start of the edit, which
	 * might be the edited value from the previous edit and NOT the actual
	 * value.
	 */
	private Double original;

	private Double text;

	/**
	 * Construct a new ViewData in editing mode.
	 * 
	 * @param text
	 *            the text to edit
	 */
	public ViewData(Double text) {
	    this.original = text;
	    this.text = text;
	    this.isEditing = true;
	    this.isEditingAgain = false;
	}

	@Override
	public boolean equals(Object o) {
	    if (o == null) {
		return false;
	    }
	    final ViewData vd = (ViewData) o;
	    return equalsOrBothNull(original, vd.original) && equalsOrBothNull(text, vd.text) && isEditing == vd.isEditing && isEditingAgain == vd.isEditingAgain;
	}

	public Double getOriginal() {
	    return original;
	}

	public Double getText() {
	    return text;
	}

	@Override
	public int hashCode() {
	    return original.hashCode() + text.hashCode() + Boolean.valueOf(isEditing).hashCode() * 29 + Boolean.valueOf(isEditingAgain).hashCode();
	}

	public boolean isEditing() {
	    return isEditing;
	}

	public boolean isEditingAgain() {
	    return isEditingAgain;
	}

	public void setEditing(boolean isEditing) {
	    final boolean wasEditing = this.isEditing;
	    this.isEditing = isEditing;

	    // This is a subsequent edit, so start from where we left off.
	    if (!wasEditing && isEditing) {
		isEditingAgain = true;
		original = text;
	    }
	}

	public void setText(Double text) {
	    this.text = text;
	}

	private boolean equalsOrBothNull(Object o1, Object o2) {
	    return (o1 == null) ? o2 == null : o1.equals(o2);
	}
    }

    private static Template template;

    private static NumberFormat fmt = NumberFormat.getFormat("##0.##");

    private final SafeHtmlRenderer<String> renderer;

    /**
     * Construct a new EditTextCell that will use a
     * {@link SimpleSafeHtmlRenderer}.
     */
    public EditDoubleCell() {
	this(SimpleSafeHtmlRenderer.getInstance());
    }

    /**
     * Construct a new EditTextCell that will use a given
     * {@link SafeHtmlRenderer} to render the value when not in edit mode.
     * 
     * @param renderer
     *            a {@link SafeHtmlRenderer SafeHtmlRenderer<String>} instance
     */
    public EditDoubleCell(SafeHtmlRenderer<String> renderer) {
	super("click", "keyup", "keydown", "blur");
	if (template == null) {
	    template = GWT.create(Template.class);
	}
	if (renderer == null) {
	    throw new IllegalArgumentException("renderer == null");
	}
	this.renderer = renderer;
    }

    @Override
    public boolean isEditing(Context context, Element parent, Double value) {
	final ViewData viewData = getViewData(context.getKey());
	return viewData == null ? false : viewData.isEditing();
    }

    @Override
    public void onBrowserEvent(Context context, Element parent, Double value, NativeEvent event, ValueUpdater<Double> valueUpdater) {
	final Object key = context.getKey();
	ViewData viewData = getViewData(key);
	if (viewData != null && viewData.isEditing()) {
	    // Handle the edit event.
	    editEvent(context, parent, value, viewData, event, valueUpdater);
	} else {
	    final String type = event.getType();
	    final int keyCode = event.getKeyCode();
	    final boolean enterPressed = "keyup".equals(type) && keyCode == KeyCodes.KEY_ENTER;
	    if ("click".equals(type) || enterPressed) {
		// Go into edit mode.
		if (viewData == null) {
		    viewData = new ViewData(value);
		    setViewData(key, viewData);
		} else {
		    viewData.setEditing(true);
		}
		edit(context, parent, value);
	    }
	}
    }

    @Override
    public void render(Context context, Double value, SafeHtmlBuilder sb) {
	// Get the view data.
	final Object key = context.getKey();
	ViewData viewData = getViewData(key);
	if (viewData != null && !viewData.isEditing() && value != null && value.equals(viewData.getText())) {
	    clearViewData(key);
	    viewData = null;
	}

	if (viewData != null) {
	    final Double text = viewData.getText();
	    if (viewData.isEditing()) {
		/*
		 * Do not use the renderer in edit mode because the value of a
		 * text input element is always treated as text. SafeHtml isn't
		 * valid in the context of the value attribute.
		 */
		sb.append(template.input(text.toString()));
	    } else {
		// The user pressed enter, but view data still exists.
		sb.append(renderer.render(text.toString()));
	    }
	} else if (value != null) {
	    sb.append(renderer.render(fmt.format(value)));
	}
    }

    @Override
    public boolean resetFocus(Context context, Element parent, Double value) {
	if (isEditing(context, parent, value)) {
	    getInputElement(parent).focus();
	    return true;
	}
	return false;
    }

    /**
     * Convert the cell to non-edit mode.
     * 
     * @param context
     *            the context of the cell
     * @param parent
     *            the parent Element
     * @param value
     *            the value associated with the cell
     */
    private void cancel(Context context, Element parent, Double value) {
	clearInput(getInputElement(parent));
	setValue(context, parent, value);
    }

    /**
     * Clear selected from the input element. Both Firefox and IE fire spurious
     * onblur events after the input is removed from the DOM if selection is not
     * cleared.
     * 
     * @param input
     *            the input element
     */
    private native void clearInput(Element input) /*-{
		if (input.selectionEnd)
			input.selectionEnd = input.selectionStart;
		else if ($doc.selection)
			$doc.selection.clear();
    }-*/;

    /**
     * Commit the current value.
     * 
     * @param context
     *            the context of the cell
     * @param parent
     *            the parent Element
     * @param viewData
     *            the {@link ViewData} object
     * @param valueUpdater
     *            the {@link ValueUpdater}
     */
    private void commit(Context context, Element parent, ViewData viewData, ValueUpdater<Double> valueUpdater) {
	final String value = updateViewData(parent, viewData, false);
	clearInput(getInputElement(parent));
	setValue(context, parent, viewData.getOriginal());
	if (valueUpdater != null) {
	    valueUpdater.update(Double.parseDouble(value));
	}
    }

    private void editEvent(Context context, Element parent, Double value, ViewData viewData, NativeEvent event, ValueUpdater<Double> valueUpdater) {
	final String type = event.getType();
	final boolean keyUp = "keyup".equals(type);
	final boolean keyDown = "keydown".equals(type);
	if (keyUp || keyDown) {
	    final int keyCode = event.getKeyCode();
	    if (keyUp && keyCode == KeyCodes.KEY_ENTER) {
		// Commit the change.
		commit(context, parent, viewData, valueUpdater);
	    } else if (keyUp && keyCode == KeyCodes.KEY_ESCAPE) {
		// Cancel edit mode.
		final Double originalText = viewData.getOriginal();
		if (viewData.isEditingAgain()) {
		    viewData.setText(originalText);
		    viewData.setEditing(false);
		} else {
		    setViewData(context.getKey(), null);
		}
		cancel(context, parent, value);
	    } else {
		// Update the text in the view data on each key.
		updateViewData(parent, viewData, true);
	    }
	} else if ("blur".equals(type)) {
	    // Commit the change. Ensure that we are blurring the input element
	    // and
	    // not the parent element itself.
	    final EventTarget eventTarget = event.getEventTarget();
	    if (Element.is(eventTarget)) {
		final Element target = Element.as(eventTarget);
		if ("input".equals(target.getTagName().toLowerCase())) {
		    commit(context, parent, viewData, valueUpdater);
		}
	    }
	}
    }

    /**
     * Get the input element in edit mode.
     */
    private InputElement getInputElement(Element parent) {
	return parent.getFirstChild().<InputElement> cast();
    }

    /**
     * Update the view data based on the current value.
     * 
     * @param parent
     *            the parent element
     * @param viewData
     *            the {@link ViewData} object to update
     * @param isEditing
     *            true if in edit mode
     * @return the new value
     */
    private String updateViewData(Element parent, ViewData viewData, boolean isEditing) {
	final InputElement input = (InputElement) parent.getFirstChild();
	String value = input.getValue();
	try {
	    viewData.setText(Double.valueOf(value));
	} catch (final NumberFormatException nfe) {
	    value = "0";
	}
	viewData.setEditing(isEditing);
	return value;
    }

    /**
     * Convert the cell to edit mode.
     * 
     * @param context
     *            the {@link Context} of the cell
     * @param parent
     *            the parent element
     * @param value
     *            the current value
     */
    protected void edit(Context context, Element parent, Double value) {
	setValue(context, parent, value);
	final InputElement input = getInputElement(parent);
	input.focus();
	input.select();
    }
}
